﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PlanetSim.ParticleSimulator
{
    public class Simulation
    {
        protected List<Particle> particles;
        public float Width { get { return Parameters.Width; } }
        public float Height { get { return Parameters.Height; } }

        public SimulationParameters Parameters;

        public Simulation(SimulationParameters parameters)
        {
            this.Parameters = parameters;
            particles = new List<Particle>();
        }

        /// <summary>
        /// Runs a single increment over the given delta
        /// </summary>
        /// <param name="delta">The amount of time to simulate</param>
        public void Simulate(float delta)
        {
            foreach (Particle p in particles)
            {
                p.Density = Parameters.InitialDensity;
                p.Force = Vector.Zero;
            }

            LinkedList<ParticlePair> pairs = FindPairs();

            foreach (ParticlePair pair in pairs)
            {
                UpdateDensities(pair);
            }

            foreach (ParticlePair pair in pairs)
            {
                UpdateForces(pair);
            }

            foreach (Particle p in particles)
            {
                ApplyBoundaryRules(p);
                MoveParticle(p, delta);
                //p.Position = new Vector((p.Position.X + Width) % Width, (p.Position.Y + Height) % Height);
            }
        }

        /// <summary>
        /// Gets a list of particle data to use for rendering
        /// </summary>
        /// <returns>A list of particles</returns>
        public List<ParticleData> GetParticleData()
        {
            List<ParticleData> data = new List<ParticleData>(particles.Count);
            foreach (Particle p in particles)
            {
                data.Add(new ParticleData(p));
            }
            return data;
        }

        public void AddParticle(ParticleData data)
        {
            particles.Add(data.BuildParticle(this));
        }

        protected LinkedList<ParticlePair> FindPairs()
        {
            LinkedList<ParticlePair> pairs = new LinkedList<ParticlePair>();

            for (int i = 0; i < particles.Count; i++)
            {
                for (int j = i + 1; j < particles.Count; j++)
                {
                    float distanceSquared = Vector.DistanceSquared(particles[i].Position, particles[j].Position);
                    if (distanceSquared < Parameters.SmoothLengthSquared)
                    {
                        pairs.AddLast(new ParticlePair(particles[i], particles[j], distanceSquared));
                    }
                }
            }

            return pairs;
        }

        protected float ParticleDistance(Particle a, Particle b)
        {
            return Vector.Distance(a.Position, b.Position);
        }

        protected void UpdateDensities(ParticlePair pair)
        {
            Particle a = pair.A;
            Particle b = pair.B;
            float distanceSquared = pair.DistanceSquared;

            float densityCoefficient = Parameters.WPoly6(Parameters.SmoothLengthSquared - distanceSquared);
            a.Density += densityCoefficient * a.Mass;
            b.Density += densityCoefficient * b.Mass;
        }

        protected void UpdateForces(ParticlePair pair)
        {
            float distance = (float)Math.Sqrt(pair.DistanceSquared);
            Vector r = pair.A.Position - pair.B.Position;
            
            // pressure
            float pressureA = Parameters.GasConstant * (pair.A.Density - Parameters.RestDensity);
            float pressureB = Parameters.GasConstant * (pair.B.Density - Parameters.RestDensity);
            float pressureSum = pressureA + pressureB;
            Vector commonPressureTerm = -((pair.A.Mass + pair.B.Mass) * pressureSum / 2f) * Parameters.GradientWSpiky(r, distance);
            pair.A.Force -= commonPressureTerm / pair.B.Density;
            pair.B.Force += commonPressureTerm / pair.A.Density;

            // viscosity
            Vector velocityDifference = pair.B.Velocity - pair.A.Velocity;
            Vector commonViscosityTerm = (pair.A.Mass + pair.B.Mass) / 2f * Parameters.Viscosity * velocityDifference * Parameters.LaplacianWViscosity(distance);
            pair.A.Force += commonViscosityTerm / pair.B.Density;
            pair.B.Force -= commonViscosityTerm / pair.A.Density;
        }

        protected void MoveParticle(Particle p, float elapsedTime)
        {
            Vector oldVelocityContribution = p.Velocity * (float)Math.Pow(Parameters.DampingCoefficient, elapsedTime);
            p.Velocity = oldVelocityContribution + (p.Force / p.Density + Parameters.Gravity) * elapsedTime;
            p.Position += p.Velocity;
        }

        protected void ApplyBoundaryRules(Particle p)
        {
            if (p.Position.X < p.Radius) p.Position.X = p.Radius;
            if (p.Position.X >= Width - p.Radius) p.Position.X = Width - p.Radius;
            if (p.Position.Y < p.Radius) p.Position.Y = p.Radius;
            if (p.Position.Y >= Height - p.Radius) p.Position.Y = Height - p.Radius;
            Vector newPosition = p.Position + p.Velocity;
            if (newPosition.X < p.Radius || newPosition.X >= Width - p.Radius) p.Velocity.X = -p.Velocity.X;
            if (newPosition.Y < p.Radius || newPosition.Y >= Height - p.Radius) p.Velocity.Y = -p.Velocity.Y;
        }

        protected class ParticlePair
        {
            public Particle A;
            public Particle B;
            public float DistanceSquared;

            public ParticlePair(Particle a, Particle b, float distanceSquared)
            {
                this.A = a;
                this.B = b;
                this.DistanceSquared = distanceSquared;
            }
        }
    }
}
